﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Collections;
using System.ComponentModel;
using System.Drawing.Design;

namespace Qreed.Windows.ComponentModel
{
    /// <summary>
    /// The <see cref="PropertyBag"/> allows you to change properties used in a <see cref="PropertyGrid"/> at runtime.
    /// </summary>
    public class PropertyBag : ICustomTypeDescriptor
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="PropertyBag"/> class.
        /// </summary>
        public PropertyBag()
        {
            _items = new PropertyBagItemCollection(this);
        }

        private PropertyBagItemCollection _items;
        /// <summary>
        /// Gets the items.
        /// </summary>
        /// <value>The items.</value>
        public PropertyBagItemCollection Items
        {
            get {return this._items;}
        }

        #region Merging (UGLY but working)

        private class MergeProperty
        {
            public MergeProperty(Type t, PropertyBagMergeOptions options)
            {
                Type = t;
                Options = options;
            }

            public void Add(string uniqueName, PropertyBagItem item)
            {
                if (!IsMultiValue && Items.Count > 0)
                {
                    PropertyBagItem prev = Items[0];

                    if ((prev.Value == null && item.Value != null) ||
                        (prev.Value != null && item.Value == null))
                    {
                        IsMultiValue = true;
                    }  
                    else if ((prev.Value != null && item.Value != null) &&
                             !prev.Value.Equals(item.Value))
                        IsMultiValue = true;
                }

                if (!item.IsBrowsable)
                    IsBrowsable = false;

                if (item.IsReadOnly)
                    IsReadOnly = true;

                UniqueNames.Add(uniqueName);
                Items.Add(item);
            }

            public PropertyBagItem ToPropertyBagItem(string multiValueText)
            {
                if(Items.Count == 0)
                    return null;

                PropertyBagItem firstItem = Items[0];

                string name = firstItem.Name;
                string displayName = firstItem.DisplayName;
                string description = firstItem.Description;
                string category = firstItem.Category;

                TypeConverter typeConverter = firstItem.TypeConverter;
                UITypeEditor typeEditor = firstItem.TypeEditor;

                if (Items.Count > 1 && (IsMultiValue || 
                    (Options & PropertyBagMergeOptions.AlwaysCreateExpandable) != 0))
                {
                    PropertyBagExpandableItem ret = new PropertyBagExpandableItem(name, displayName,
                                                                                  multiValueText, multiValueText,
                                                                                  category, description,
                                                                                  IsReadOnly, IsBrowsable,
                                                                                  Type);

                    ret.TypeEditor = typeEditor;

                    List<object> tags = new List<object>();

                    for (int i = 0; i < Items.Count; i++)
                    {
                        string uniqueName = UniqueNames[i];
                        PropertyBagItem item = Items[i];

                        PropertyBagItem newItem = new PropertyBagItem(name, uniqueName, item.Value, item.DefaultValue,
                                                                      item.Category, item.Description, IsReadOnly, IsBrowsable,
                                                                      Type);

                        newItem.TypeConverter = typeConverter;
                        newItem.TypeEditor = typeEditor;

                        newItem.Tag = item.Tag;

                        tags.Add(item.Tag);

                        ret.Items.Add(newItem);
                    }

                    ret.Tag = tags.ToArray();

                    return ret;
                }
                else
                {
                    PropertyBagItem newItem = new PropertyBagItem(name, displayName, firstItem.Value, firstItem.DefaultValue,
                                                                  category, description, IsReadOnly, IsBrowsable, Type);

                    newItem.Tag = firstItem.Tag;
                    newItem.TypeConverter = typeConverter;
                    newItem.TypeEditor = typeEditor;

                    return newItem;
                }
            }

            public bool IsMultiValue;
            public bool IsBrowsable = true;
            public bool IsReadOnly = false;

            public Type Type;
            public PropertyBagMergeOptions Options;
            
            public List<string> UniqueNames = new List<string>();
            public List<PropertyBagItem> Items = new List<PropertyBagItem>();
        }

        public static PropertyBag Merge(string uniqueNameProperty, PropertyBag[] bags, PropertyBagMergeOptions options)
        {
            PropertyBag result = new PropertyBag();

            if (string.IsNullOrEmpty(uniqueNameProperty))
                throw (new ArgumentNullException("uniqueNameProperty"));

            if (bags == null || bags.Length == 0)
                return null;

            MergeValidate(uniqueNameProperty, bags);

            // Grab all available property names and their types
            // This will also ensure all properties are of the correct type
            Dictionary<string, MergeProperty> properties = MergeBuildPropertyDictionary(uniqueNameProperty, bags, options);

            foreach (string propertyName in properties.Keys)
            {
                MergeProperty mp = properties[propertyName];
                PropertyBagItem item = mp.ToPropertyBagItem("<multiple values>");

                result.Items.Add(item);
            }

            return result;
        }

        private static Dictionary<string, MergeProperty> MergeBuildPropertyDictionary(string uniqueNameProperty, PropertyBag[] bags, PropertyBagMergeOptions options)
        {
            Dictionary<string, MergeProperty> properties = new Dictionary<string, MergeProperty>();
            
            for (int i = 0; i < bags.Length; i++)
            {
                PropertyBag bag = bags[i];
                string uniqueValue = bag.Items[uniqueNameProperty].Value.ToString();

                // Grab the property names and types 

                for (int j = 0; j < bag.Items.Count; j++)
                {
                    PropertyBagItem item = bag.Items[j];
                    MergeProperty mp = null;

                    if ((options & PropertyBagMergeOptions.IncludeUniqueField) == 0 && item.Name == uniqueNameProperty)
                        continue;

                    if (properties.ContainsKey(item.Name))
                    {
                        mp = properties[item.Name];

                        // Check if the property is of the same type as the one we've stored earlier
                        if (mp.Type != item.Type)
                            throw (new ApplicationException("Property " + item.Name + " of PropertyBag at index " + i.ToString() + " is of type " + item.Type.Name + ". Expected " + properties[item.Name].Type.Name + "."));
                    }
                    else
                    {
                        mp = new MergeProperty(item.Type, options);
                        properties.Add(item.Name, mp);
                    }

                    mp.Add(uniqueValue, item);
                }
            }

            return properties;
        }

        private static void MergeValidate(string uniqueNameProperty, PropertyBag[] bags)
        {
            for (int i = 0; i < bags.Length; i++)
            {
                // Validate

                PropertyBag bag = bags[i];
                PropertyBagItem uniqueItem = bag.Items[uniqueNameProperty];

                if (uniqueItem == null)
                    throw (new ApplicationException("PropertyBag with index " + i.ToString() + " does not contain the property " + uniqueNameProperty + "."));

                if (uniqueItem.Value == null)
                    throw (new ApplicationException("PropertyBag with index " + i.ToString() + " contains a null value as unique name."));

                string uniqueValue = uniqueItem.Value.ToString();

                if (string.IsNullOrEmpty(uniqueValue))
                    throw (new ApplicationException("The property " + uniqueNameProperty + " of PropertyBag with index " + i.ToString() + " is empty."));
            }
        }

        #endregion

        #region Events

        /// <summary>
        /// Occurs when a value has been set.
        /// </summary>
        public event EventHandler<PropertyBagEventArgs> SetValue;

        /// <summary>
        /// Raises the <see cref="E:SetValue"/> event.
        /// </summary>
        /// <param name="e">The <see cref="Qreed.Windows.ComponentModel.PropertyBagEventArgs"/> instance containing the event data.</param>
        public virtual void OnSetValue(PropertyBagEventArgs e)
        {
            if (SetValue != null)
                SetValue(this, e);
        }

        /// <summary>
        /// Occurs when a value is beeing requested by the property grid.
        /// </summary>
        public event EventHandler<PropertyBagEventArgs> GetValue;

        /// <summary>
        /// Raises the <see cref="E:GetValue"/> event.
        /// </summary>
        /// <param name="e">The <see cref="Qreed.Windows.ComponentModel.PropertyBagEventArgs"/> instance containing the event data.</param>
        public virtual void OnGetValue(PropertyBagEventArgs e)
        {
            if (GetValue != null)
                GetValue(this, e);
        }

        #endregion

        #region ICustomTypeDescriptor Members
        
        /// <summary>
        /// Returns a collection of custom attributes for this instance of a component.
        /// </summary>
        /// <returns>
        /// An <see cref="T:System.ComponentModel.AttributeCollection"></see> containing the attributes for this object.
        /// </returns>
        public AttributeCollection GetAttributes()
        {
            return TypeDescriptor.GetAttributes(this, true);
        }
        
        /// <summary>
        /// Returns the class name of this instance of a component.
        /// </summary>
        /// <returns>
        /// The class name of the object, or null if the class does not have a name.
        /// </returns>
        public string GetClassName()
        {
            return TypeDescriptor.GetClassName(this, true);
        }
        
        /// <summary>
        /// Returns the name of this instance of a component.
        /// </summary>
        /// <returns>
        /// The name of the object, or null if the object does not have a name.
        /// </returns>
        public string GetComponentName()
        {
            return TypeDescriptor.GetComponentName(this, true);
        }
        
        /// <summary>
        /// Returns a type converter for this instance of a component.
        /// </summary>
        /// <returns>
        /// A <see cref="T:System.ComponentModel.TypeConverter"></see> that is the converter for this object, or null if there is no <see cref="T:System.ComponentModel.TypeConverter"></see> for this object.
        /// </returns>
        public TypeConverter GetConverter()
        {
            return TypeDescriptor.GetConverter(this, true);
         //   return new PropertyBagConverter(this);
        }
        
        /// <summary>
        /// Returns the default event for this instance of a component.
        /// </summary>
        /// <returns>
        /// An <see cref="T:System.ComponentModel.EventDescriptor"></see> that represents the default event for this object, or null if this object does not have events.
        /// </returns>
        public EventDescriptor GetDefaultEvent()
        {
            return TypeDescriptor.GetDefaultEvent(this, true);
        }
        
        /// <summary>
        /// Returns the default property for this instance of a component.
        /// </summary>
        /// <returns>
        /// A <see cref="T:System.ComponentModel.PropertyDescriptor"></see> that represents the default property for this object, or null if this object does not have properties.
        /// </returns>
        public PropertyDescriptor GetDefaultProperty()
        {
            return TypeDescriptor.GetDefaultProperty(this, true);
        }
        
        /// <summary>
        /// Returns an editor of the specified type for this instance of a component.
        /// </summary>
        /// <param name="editorBaseType">A <see cref="T:System.Type"></see> that represents the editor for this object.</param>
        /// <returns>
        /// An <see cref="T:System.Object"></see> of the specified type that is the editor for this object, or null if the editor cannot be found.
        /// </returns>
        public object GetEditor(Type editorBaseType)
        {
            return TypeDescriptor.GetEditor(this, editorBaseType, true);
        }
        
        /// <summary>
        /// Returns the events for this instance of a component using the specified attribute array as a filter.
        /// </summary>
        /// <param name="attributes">An array of type <see cref="T:System.Attribute"></see> that is used as a filter.</param>
        /// <returns>
        /// An <see cref="T:System.ComponentModel.EventDescriptorCollection"></see> that represents the filtered events for this component instance.
        /// </returns>
        public EventDescriptorCollection GetEvents(Attribute[] attributes)
        {
            return TypeDescriptor.GetEvents(this, attributes, true);
        }
        
        /// <summary>
        /// Returns the events for this instance of a component.
        /// </summary>
        /// <returns>
        /// An <see cref="T:System.ComponentModel.EventDescriptorCollection"></see> that represents the events for this component instance.
        /// </returns>
        public EventDescriptorCollection GetEvents()
        {
            return TypeDescriptor.GetEvents(this, true);
        }
        
        /// <summary>
        /// Returns the properties for this instance of a component using the attribute array as a filter.
        /// </summary>
        /// <param name="attributes">An array of type <see cref="T:System.Attribute"></see> that is used as a filter.</param>
        /// <returns>
        /// A <see cref="T:System.ComponentModel.PropertyDescriptorCollection"></see> that represents the filtered properties for this component instance.
        /// </returns>
        public PropertyDescriptorCollection GetProperties(Attribute[] attributes)
        {
            return Items.GetProperties(attributes);
        }
        
        /// <summary>
        /// Returns the properties for this instance of a component.
        /// </summary>
        /// <returns>
        /// A <see cref="T:System.ComponentModel.PropertyDescriptorCollection"></see> that represents the properties for this component instance.
        /// </returns>
        public PropertyDescriptorCollection GetProperties()
        {
            return GetProperties(null);
        }
        
        /// <summary>
        /// Returns an object that contains the property described by the specified property descriptor.
        /// </summary>
        /// <param name="pd">A <see cref="T:System.ComponentModel.PropertyDescriptor"></see> that represents the property whose owner is to be found.</param>
        /// <returns>
        /// An <see cref="T:System.Object"></see> that represents the owner of the specified property.
        /// </returns>
        public object GetPropertyOwner(PropertyDescriptor pd)
        {
            return this;
        }

        #endregion
    }

    /// <summary>
    /// Options for the <see cref="PropertyBag.Merge"/> function
    /// </summary>
    [Flags]
    public enum PropertyBagMergeOptions
    {
        /// <summary>
        /// 
        /// </summary>
        None,
        /// <summary>
        /// Include the unique field property in the merged result
        /// </summary>
        IncludeUniqueField,
        /// <summary>
        /// Always create an expandable node even if the values of all items are the same
        /// </summary>
        AlwaysCreateExpandable
    }

}
